#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <syserr.h>
#include <device.h>
#include <utils.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <netinet/tcp.h>
#include "device.h"
#include "netcore.h"
#include "evtloop.h"
#include "dev/stream.h"
#include "printk.h"
#include "softtimer.h"

#define PEERIP_TRYNR    (2)

#define to_client(stream_dev) \
    container_of(stream_dev, struct tcp_client_device, stream_dev)

struct tcp_client_device
{
    struct stream_device    stream_dev;
    struct tcp_client_cfg   cfg;
    ip_addr_t               local_addr_real;
    int                     peer_index;
    struct soft_timer       conn_tmr;             /* connect timeout check    */
    struct soft_timer       eth_tmr;              /* ethernet interface check */
    u32                     eth_link_down_second;
    u32                     tries;
};

static int tcp_client_conn(struct tcp_client_device *client);
static void emit_conn_signal(struct stream_device *stream_dev,
                             enum DEVICE_EVENT e, int err);

static inline void to_next_peer(struct tcp_client_device *client)
{
    client->tries = PEERIP_TRYNR;
}
static void conn_tmr_cb(void *data)
{
    struct tcp_client_device *client = data;
    device_t *dev = &client->stream_dev.dev;

    switch (dev_status(dev))
    {
    case DEVICE_STATUS_IDLE:
        dev_status(dev) = DEVICE_STATUS_CONNECTING;
        tcp_client_conn(client);
        break;
    case DEVICE_STATUS_CONNECTING:
        device_restart(dev);
        break;
    default:
        break;
    }
}

static bool should_reconnect_while_linkdown(struct tcp_client_device *client)
{
    device_t *dev = &client->stream_dev.dev;
    bool reconect = false;

    switch (dev_status(dev))
    {
    case DEVICE_STATUS_IDLE:
        reconect = true;
        break;
    case DEVICE_STATUS_CONNECTING:
        reconect = false;
        break;
    case DEVICE_STATUS_CONNECTED:
        client->eth_link_down_second++;
        if (client->cfg.eth_chk_timeout > 0 &&
            client->eth_link_down_second > client->cfg.eth_chk_timeout)
            reconect = true;
        break;
    default:
        reconect = true;
        break;
    }

    return reconect;
}

static void eth_tmr_cb(void *data)
{
    struct tcp_client_device *client = data;
    device_t *dev = &client->stream_dev.dev;

    int ret = net_get_if_status(client->cfg.ethname);
    if (ret > 0)
    {
        client->eth_link_down_second = 0;
        if (dev_status(dev) == DEVICE_STATUS_IDLE && dev_fd(dev) >= 0)
        {
            /* netlink up, request peerip...
             */
            emit_conn_signal(&client->stream_dev, DE_CONN, -ETIMEDOUT);
            soft_timer_start(&client->conn_tmr, 0, 0);
        }
    }
    else if (ret == 0)
    {
        if (should_reconnect_while_linkdown(client))
        {
            DEV_PI("<%s>: detect [%s] link down, will reconnect...\n",
                   dev_name(dev), client->cfg.ethname);

            client->eth_link_down_second = 0;
            soft_timer_stop(&client->eth_tmr);

            emit_conn_signal(&client->stream_dev, DE_DISCONN, -EIO);

            /* reconnect...
             */
            dev_status(dev) = DEVICE_STATUS_CONNECTING;
            soft_timer_start(&client->conn_tmr, 0, 0);
        }
    }
}

static void emit_conn_signal(struct stream_device *stream_dev,
                             enum DEVICE_EVENT e, int err)
{
    device_t *dev = to_dev(stream_dev);
    struct tcp_client_device *client = to_client(stream_dev);

    if (dev->usr_cb && dev->usr_cb->ev_cb)
    {
        dev_conn_evt_args_t arg;

        arg.err = err;
        ip_addr_copy(arg.ip, client->cfg.remote_ip[client->peer_index]);
        arg.port = client->cfg.remote_port[client->peer_index];
        arg.peer = dev;

        dev->usr_cb->ev_cb(dev, e, &arg);
    }
}

static void tcp_client_conn_req(struct tcp_client_device *client)
{
    int error;
    socklen_t errorsize = sizeof(int);
    device_t *dev = to_dev(&client->stream_dev);

    if (dev->delayed_error)
    {
        error = dev->delayed_error;
        dev->delayed_error = 0;
    }
    else
    {
        assert(dev_fd(dev) >= 0);
        getsockopt(dev_fd(dev),
                   SOL_SOCKET,
                   SO_ERROR,
                   &error,
                   &errorsize);
        error = -error;
    }
    if (error == -EINPROGRESS) return;

    soft_timer_stop(&client->conn_tmr);
    ev__io_stop(dev_loop(dev), &dev->io, EPOLLOUT);

    if (error < 0)
    {
        DEV_PE("<%s>: connect fault [%s]!\n", dev_name(dev), strerror(-error));

        if (error == -EINVAL)
            to_next_peer(client);
        dev_status(dev) = DEVICE_STATUS_CONNECTING;
        soft_timer_start(&client->conn_tmr, client->cfg.conn_timeout * HZ, 0);
        return;
    }

    emit_conn_signal(&client->stream_dev, DE_CONN, 0);

    dev_status(dev) = DEVICE_STATUS_CONNECTED;
    ev__io_start(dev_loop(dev), &dev->io, EPOLLIN);
}

static void tcp_client_eof(struct stream_device *stream_dev)
{
    device_t *dev = to_dev(stream_dev);
    struct tcp_client_device *client = to_client(stream_dev);

    if (dev_status(dev) != DEVICE_STATUS_CONNECTED) return;
    soft_timer_stop(&client->eth_tmr);

    emit_conn_signal(stream_dev, DE_DISCONN, 0);

    soft_timer_stop(&client->conn_tmr);
    ev__io_stop(dev_loop(dev), &dev->io, EPOLLOUT);
    dev_status(dev) = DEVICE_STATUS_IDLE;

    if (client->cfg.autoconn)
    {
        DEV_PI("<%s>: disconnect, reconnect...\n", dev_name(dev));

        /* connect immediately in next loop
         */
        dev_status(dev) = DEVICE_STATUS_CONNECTING;
        soft_timer_start(&client->conn_tmr, 0, 0);
    }
}

static void tcp_client_poll_in(struct stream_device *stream_dev)
{
    device_t *dev = to_dev(stream_dev);
    struct tcp_client_device *client = to_client(stream_dev);

    if (dev_status(dev) == DEVICE_STATUS_CONNECTING)
    {
        tcp_client_conn_req(client);
        return;
    }

    assert(dev_status(dev) == DEVICE_STATUS_CONNECTED);

    int l = stream_poll_read(stream_dev);
    if (l == 0)
    {
        tcp_client_eof(stream_dev);
    }
    else
    {
        if (dev->usr_cb && dev->usr_cb->ev_cb)
            dev->usr_cb->ev_cb(dev, DE_RCV, NULL);
    }
}
static void tcp_client_poll_out(struct stream_device *stream_dev)
{
    device_t *dev = to_dev(stream_dev);
    struct tcp_client_device *client = to_client(stream_dev);

    if (dev_status(dev) == DEVICE_STATUS_CONNECTING)
        tcp_client_conn_req(client);
}

static int tcp_client_init(struct stream_device *stream_dev)
{
    device_t *dev = to_dev(stream_dev);
    struct tcp_client_device *client = to_client(stream_dev);

    soft_timer_add(dev_loop(dev), &client->conn_tmr, "tmr_tcpconn",
                   conn_tmr_cb, client);
    soft_timer_add(dev_loop(dev), &client->eth_tmr,  "tmr_eth",
                   eth_tmr_cb, client);
    return 0;
}

static int tcp_client_conn(struct tcp_client_device *client)
{
    device_t *dev = to_dev(&client->stream_dev);

    if (++client->tries > PEERIP_TRYNR)
    {
        client->tries = 0;
        if (++client->peer_index >= REMOTE_PARA_CNT)
        {
            client->peer_index = 0;
            emit_conn_signal(&client->stream_dev, DE_CONN, -ETIMEDOUT);
        }
    }

    ip_addr_t *ip = &client->cfg.remote_ip[client->peer_index];
    u16 port = client->cfg.remote_port[client->peer_index];
    DEV_PI("<%s>: connect to %s:%d, timeout[%ds]...\n", dev_name(dev),
           ipaddr_ntoa(ip), port, client->cfg.conn_timeout);

    dev->delayed_error = 0;

    int ret = net_conn(dev, ip, port);
    if (ret == -ECONNREFUSED || ret == -EINVAL)
    {
        dev->delayed_error = ret;
        ret = 0;
    }
    if (ret < 0)
    {
        int error = 0;
        socklen_t len = sizeof(error);
        getsockopt(dev_fd(dev), SOL_SOCKET, SO_ERROR, &error, &len);

        DEV_PE("<%s>: connect fault [%s]!\n", dev_name(dev), strerror(error));

        dev_status(dev) = DEVICE_STATUS_CONNECTING;
        soft_timer_start(&client->conn_tmr, client->cfg.conn_timeout * HZ, 0);
        return -1;
    }
    ev__io_stop(dev_loop(dev), &dev->io, EPOLLIN);
    ev__io_start(dev_loop(dev), &dev->io, EPOLLOUT);

    soft_timer_start(&client->conn_tmr, client->cfg.conn_timeout * HZ, 0);
    return 0;
}

static int tcp_client_open(struct stream_device *stream_dev)
{
    int sock;
    struct tcp_client_device *client = to_client(stream_dev);
    struct tcp_client_cfg *cfg = &client->cfg;

    DEV_PD("<%s>: new socket, dev[%s], ip[%s], port[%d]...\n",
           dev_name(to_dev(stream_dev)), cfg->ethname ? cfg->ethname : "default",
           ipaddr_ntoa(&cfg->local_ip), cfg->local_port);

    sock = socket_new(to_dev(stream_dev), cfg->ethname, SOCK_STREAM,
                      &cfg->local_ip, cfg->local_port);
    if (sock < 0)
        return -ENXIO;

    if (cfg->ethname)
    {
        net_get_sock_ip(sock, cfg->ethname, &client->local_addr_real);
        DEV_PD("<%s>: local addr:%s\n", dev_name(to_dev(stream_dev)),
               ipaddr_ntoa(&client->local_addr_real));
    }
    ev__io_init(&stream_dev->dev.io, device_io, sock);

    /* connect in main loop
     */
    dev_status(&stream_dev->dev) = DEVICE_STATUS_IDLE;
    if (cfg->ethname)
        soft_timer_start(&client->eth_tmr, 0, HZ);
    else
        soft_timer_start(&client->conn_tmr, 0, 0);
    return sock;
}

static int tcp_client_ctrl(struct stream_device *stream_dev, u32 cmd, void *args)
{
    struct tcp_client_device *client = to_client(stream_dev);

    switch (cmd)
    {
    case DEV_CTRL_CMD_GET_CFG:
        memcpy(args, &client->cfg, sizeof(struct tcp_client_cfg));
        break;
    case DEV_CTRL_CMD_SET_CFG:
        if (memcmp(&client->cfg, args, sizeof(struct tcp_client_cfg)))
            memcpy(&client->cfg, args, sizeof(struct tcp_client_cfg));
        break;
    case DEV_CTRL_CMD_GET_REAL_LOCALADDR:
        *(u32 *)args = ip_addr_get_ip4_u32(&client->local_addr_real);
        break;
    case DEV_CTRL_CMD_MARK_PEER:
        client->tries = 0;
        break;
    }
    return 0;
}

static void tcp_client_release(struct stream_device *stream_dev)
{
    struct tcp_client_device *client = to_client(stream_dev);

    soft_timer_del(&client->conn_tmr);
    soft_timer_del(&client->eth_tmr);
}

static const struct stream_operations tcp_client_ops =
{
    .init   = tcp_client_init,
    .open   = tcp_client_open,
    .ctrl   = tcp_client_ctrl,
    .release = tcp_client_release,

    .on_poll_in  = tcp_client_poll_in,
    .on_poll_out = tcp_client_poll_out,
    .on_eof = tcp_client_eof,
};

static const struct tcp_client_cfg tcp_client_cfg_def =
{
    .ethname        = "eth0",
    .local_ip       = IPADDR4_INIT(0),
    .remote_ip[0]   = IPADDR4_INIT(0),
    .remote_ip[1]   = IPADDR4_INIT(0),
    .local_port     = 0,
    .remote_port[0] = 0,
    .remote_port[1] = 0,
    .autoconn       = true,
    .conn_timeout   = 20,
    .eth_chk_timeout = 10,
};

int tcp_client_dev_add(const char *name, const struct tcp_client_cfg *cfg)
{
    int ret;
    struct tcp_client_device *client = kmalloc(sizeof(struct tcp_client_device));

    if (!client)
        return -ENOMEM;

    memset(client, 0, sizeof(struct tcp_client_device));
    if (!cfg) cfg = &tcp_client_cfg_def;
    memcpy(&client->cfg, cfg, sizeof(struct tcp_client_cfg));

    client->stream_dev.ops = &tcp_client_ops;

    ret = stream_device_register(&client->stream_dev, name, NULL);
    if (ret < 0)
        kfree(client);

    return ret;
}
void tcp_client_del(device_t *dev)
{
    struct stream_device *stream_dev = to_stream_dev(dev);
    struct tcp_client_device *client = to_client(stream_dev);

    stream_device_unregister(stream_dev);
    kfree(client);
}

